﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Matrix = Microsoft.Xna.Framework.Matrix;

namespace Babylon.Toolbox
{
    public class ParticleSystem
    {
        static readonly Random Random = new Random();

        public GraphicsDevice Device { get; set; }
        public ParticleSettings Settings { get; set; }
        private readonly ParticleEffect _effect;

        /// <summary>
        /// Get or set the default position of emission
        /// </summary>
        public Vector3 Position { get; set; }

        /// <summary>
        /// Get or set the emitter object
        /// </summary>
        public SceneObject EmitterObject { get; set; }

        /// <summary>
        /// Get or set the number of particles emitted per frame
        /// </summary>
        public int ParticlesEmittedPerFrame { get; set; }

        /// <summary>
        /// Creates an instance of the particle system
        /// </summary>
        /// <param name="device">device used to create buffers and effects</param>
        /// <param name="settings">settings to use</param>
        /// <param name="position">initial position of the emitter</param>
        /// <param name="emitterObject">emitter object</param>
        /// <param name="particlesEmittedPerFrame">number of particles emitted per frame</param>
        public ParticleSystem(GraphicsDevice device, ParticleSettings settings, Vector3 position, SceneObject emitterObject = null, int particlesEmittedPerFrame = 1)
        {
            Device = device;
            Settings = settings;
            _effect = new ParticleEffect(device);
            InitializeBuffers();

            _effect.DurationRandomness = Settings.ParticleDurationRandomness;
            _effect.EndVelocityRatio = Settings.EndVelocityRatio;
            _effect.EnvironmentForces = Settings.EnvironmentForces;
            _effect.MaxEndSize = Settings.MaxEndSize;
            _effect.MaxRotationSpeed = settings.MaxRotationSpeed;
            _effect.MaxStartSize = Settings.MaxStartSize;
            _effect.MaxTeint = Settings.MaxTeint;
            _effect.MinEndSize = Settings.MinEndSize;
            _effect.MinRotationSpeed = Settings.MinRotationSpeed;
            _effect.MinStartSize = Settings.MinStartSize;
            _effect.MinTeint = Settings.MinTeint;
            _effect.ParticleDuration = Settings.ParticleAvgDuration;
            _effect.Texture = Settings.Texture;
            _effect.AlphaRatio = Settings.AlphaRatio;

            Position = position;
            EmitterObject = emitterObject;
            ParticlesEmittedPerFrame = particlesEmittedPerFrame;
        }

        private DynamicVertexBuffer _vb;
        private IndexBuffer _ib;
        private ParticleVertex[] _vertices;
        private void InitializeBuffers()
        {
            _vertices = new ParticleVertex[4 * Settings.MaxParticleCount];
            var indices = new ushort[6 * Settings.MaxParticleCount];
            for (int i = 0; i < Settings.MaxParticleCount; i++)
            {
                _vertices[i * 4 + 0] = new ParticleVertex { Corner = new Vector2(-1, 1) };
                _vertices[i * 4 + 1] = new ParticleVertex { Corner = new Vector2(1, 1) };
                _vertices[i * 4 + 2] = new ParticleVertex { Corner = new Vector2(-1, -1) };
                _vertices[i * 4 + 3] = new ParticleVertex { Corner = new Vector2(1, -1) };

                indices[i * 6 + 0] = (ushort)((i * 4) + 0);
                indices[i * 6 + 1] = (ushort)((i * 4) + 1);
                indices[i * 6 + 2] = (ushort)((i * 4) + 2);
                indices[i * 6 + 3] = (ushort)((i * 4) + 2);
                indices[i * 6 + 4] = (ushort)((i * 4) + 1);
                indices[i * 6 + 5] = (ushort)((i * 4) + 3);
            }

            _vb = new DynamicVertexBuffer(Device, ParticleVertex.VertexDeclaration, _vertices.Length, BufferUsage.WriteOnly);
            _ib = new IndexBuffer(Device, IndexElementSize.SixteenBits, indices.Length, BufferUsage.WriteOnly);

            _vb.SetData(0, _vertices, 0, _vertices.Length, ParticleVertex.VertexDeclaration.VertexStride);
            _ib.SetData(0, indices, 0, indices.Length);
        }

        private int _firstUsed;
        private int _usedCount;

        /// <summary>
        /// Add new particles to the system
        /// </summary>
        /// <param name="positions">initial positions of the particles</param>
        /// <param name="currentTime">current global time (in seconds)</param>
        public void AddParticles(Vector3[] positions, float currentTime)
        {
            _currentTime = currentTime;

            var toUse = Math.Min(positions.Length, Settings.MaxParticleCount - _usedCount);

            int firstFreeInBuffer = (_firstUsed + _usedCount) % Settings.MaxParticleCount;
            int firstUsedInBuffer = _firstUsed % Settings.MaxParticleCount;

            int toAddInTail = Math.Min(toUse, Settings.MaxParticleCount - firstFreeInBuffer);
            if (toAddInTail > 0)
                AddParticlesInRange(positions, 0, toAddInTail, firstFreeInBuffer);

            int toAddInHead = Math.Min(toUse - toAddInTail, firstUsedInBuffer);
            if (toAddInHead > 0)
                AddParticlesInRange(positions, toAddInTail, toAddInHead, 0);

            _usedCount += toAddInHead + toAddInTail;
        }

        /// <summary>
        /// Add new particles to the system using default emitting position
        /// </summary>
        /// <param name="count">number of particles to emmit</param>
        /// <param name="currentTime">current global time (in seconds)</param>
        public void AddParticles(int count, float currentTime)
        {
            var positions = new Vector3[count];

            Vector3 emissionPosition = Position;

            if (EmitterObject != null)
                emissionPosition = Vector3.Transform(emissionPosition, EmitterObject.World);

            for (int index = 0; index < count; index++)
            {
                positions[index] = emissionPosition;
            }

            AddParticles(positions, currentTime);
        }

        private void AddParticlesInRange(Vector3[] positions, int firstIndex, int count, int firstIndexInBuffer)
        {

            int firstInvalidated = firstIndexInBuffer * 4;
            int countInvalidated = count * 4;

            for (int i = 0; i < count; i++)
            {
                var pos = positions[i + firstIndex];
                InitParticleAt(firstIndexInBuffer + i, pos);
            }
            _vb.SetData(firstInvalidated * ParticleVertex.VertexDeclaration.VertexStride,
                _vertices, firstInvalidated, countInvalidated, ParticleVertex.VertexDeclaration.VertexStride);
        }

        private float _currentTime;
        private void InitParticleAt(int index, Vector3 initialPosition)
        {
            var initVelocity = new Vector3(
                MathHelper.Lerp(Settings.MinVelocityValues.X, Settings.MaxVelocityValues.X, (float)Random.NextDouble()),
                MathHelper.Lerp(Settings.MinVelocityValues.Y, Settings.MaxVelocityValues.Y, (float)Random.NextDouble()),
                MathHelper.Lerp(Settings.MinVelocityValues.Z, Settings.MaxVelocityValues.Z, (float)Random.NextDouble()));

            var r = new Vector4((float)Random.NextDouble(), (float)Random.NextDouble(), (float)Random.NextDouble(), (float)Random.NextDouble());
            for (int i = index * 4; i < index * 4 + 4; i += 4)
            {
                _vertices[i + 0].InitialPosition = initialPosition;
                _vertices[i + 1].InitialPosition = initialPosition;
                _vertices[i + 2].InitialPosition = initialPosition;
                _vertices[i + 3].InitialPosition = initialPosition;
                _vertices[i + 0].InitialVelocity = initVelocity;
                _vertices[i + 1].InitialVelocity = initVelocity;
                _vertices[i + 2].InitialVelocity = initVelocity;
                _vertices[i + 3].InitialVelocity = initVelocity;
                _vertices[i + 0].StartTime = _currentTime;
                _vertices[i + 1].StartTime = _currentTime;
                _vertices[i + 2].StartTime = _currentTime;
                _vertices[i + 3].StartTime = _currentTime;
                _vertices[i + 0].Random = r;
                _vertices[i + 1].Random = r;
                _vertices[i + 2].Random = r;
                _vertices[i + 3].Random = r;
            }
        }

        /// <summary>
        /// Update particles state and draw living particles
        /// </summary>
        /// <param name="gd">device to use</param>
        /// <param name="currentTime">global time in seconds</param>
        /// <param name="view">view matrix</param>
        /// <param name="proj">projection matrix</param>
        public void UpdateAndDraw(GraphicsDevice gd, float currentTime, Matrix view, Matrix proj)
        {
            Device = gd;
            _currentTime = currentTime;

            FreeEllapsedVertices();

            _effect.Device = gd;
            gd.SetVertexBuffer(_vb);
            gd.Indices = _ib;

            _effect.CurrentTime = currentTime;
            _effect.View = view;
            _effect.Proj = proj;
            _effect.Apply();
            int firstInBuffer = _firstUsed % Settings.MaxParticleCount;

            if (_usedCount != 0)
            {
                DepthStencilState currentDepthState = gd.DepthStencilState;
                BlendState currentBlendState = gd.BlendState;

                gd.DepthStencilState = DepthStencilState.DepthRead;
                gd.BlendState = Settings.BlendState;

                if (firstInBuffer + _usedCount <= Settings.MaxParticleCount)
                {
                    gd.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, firstInBuffer, _usedCount * 4, firstInBuffer * 6, _usedCount * 2);
                }
                else
                {
                    int toDrawFirst = Settings.MaxParticleCount - firstInBuffer;
                    gd.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, firstInBuffer, toDrawFirst * 4, firstInBuffer * 6, toDrawFirst * 2);
                    int toDrawSeconf = _usedCount - toDrawFirst;
                    gd.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, toDrawSeconf * 4, 0, toDrawSeconf * 2);
                }

                gd.BlendState = currentBlendState;
                gd.DepthStencilState = currentDepthState;
            }

            if (ParticlesEmittedPerFrame > 0)
                AddParticles(ParticlesEmittedPerFrame, currentTime);
        }

        private float GetAgeAt(int particleIndex)
        {
            return _currentTime - _vertices[particleIndex * 4].StartTime;
        }
        private void FreeEllapsedVertices()
        {
            while (_usedCount > 0 && GetAgeAt(_firstUsed % Settings.MaxParticleCount) > Settings.ParticleAvgDuration)
            {
                _firstUsed++;
                _usedCount--;
            }
        }
    }
}
